#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "undo.h"
#include "buffer.h"
#include "list.h"
#include "window.h"

static void move_cmd(undo_cmd_t *cmd)
{
    if (cmd == current_window->undo_cmd) {
	if (current_window->redo_cmd == NULL) {
	    list_del(&(cmd->list));
	    INIT_LIST_HEAD(&(cmd->list));
	    current_window->redo_cmd = cmd;
	} else {
	    list_move(&(cmd->list), &(current_window->redo_cmd->list));
	    current_window->redo_cmd = cmd;
	}
    } else {
	if (current_window->undo_cmd == NULL) {
	    list_del(&(cmd->list));
	    INIT_LIST_HEAD(&(cmd->list));
	    current_window->undo_cmd = cmd;
	} else {
	    list_move(&(cmd->list), &(current_window->undo_cmd->list));
	    current_window->undo_cmd = cmd;
	}
    }
}

static void buf_appends_action(undo_cmd_t *cmd)
{
    int start = cmd->buf->len;
    int len = strlen(cmd->data);

    buf_appends_noundo(cmd->buf, cmd->data);
    cmd->exec = BUF_DELETE_ACTION;
    cmd->start = start;
    cmd->end = start + len;
    move_cmd(cmd);
}

static void buf_appendc_action(undo_cmd_t *cmd)
{
    int start = cmd->buf->len;

    buf_appendc_noundo(cmd->buf, cmd->data[0]);
    cmd->exec = BUF_DELETE_ACTION;
    cmd->start = start;
    cmd->end = start + 1;
    move_cmd(cmd);
}

static void buf_inserts_action(undo_cmd_t *cmd)
{
    buf_inserts_noundo(cmd->buf, cmd->data, cmd->start);
    cmd->exec = BUF_DELETE_ACTION;
    cmd->end = cmd->start + strlen(cmd->data);
    move_cmd(cmd);
}

static void buf_insertc_action(undo_cmd_t *cmd)
{
    buf_insertc_noundo(cmd->buf, cmd->data[0], cmd->start);
    cmd->exec = BUF_DELETE_ACTION;
    cmd->end = cmd->start + 1;
    move_cmd(cmd);
}

static void buf_delete_action(undo_cmd_t *cmd)
{
    buf_delete_noundo(cmd->buf, cmd->start, cmd->end);
    cmd->exec = BUF_INSERTS_ACTION;
    move_cmd(cmd);
}

static void buf_replace_action(undo_cmd_t *cmd)
{
    int old_len = cmd->end - cmd->start;
    int len = strlen(cmd->data);
    char *old_data = NULL;

    if ((old_data = (char *)malloc(old_len + 1)) == NULL) {
	perror("Out of memory!");
	exit(1);
    }
    strncpy(old_data, cmd->buf->data + cmd->start, old_len);
    old_data[old_len] = '\0';
    
    buf_replace_noundo(cmd->buf, cmd->start, cmd->end, cmd->data);

    free(cmd->data);
    cmd->data = old_data;
    cmd->end = cmd->start + len;
    move_cmd(cmd);
}

static void buf_restore_buf_action(undo_cmd_t *cmd)
{
    if (cmd->end == 0) {
	buf_restore_buf_noundo(cmd->buf, FALSE);
	cmd->exec = BUF_REMOVE_BUF_ACTION;
    } else {
	buf_restore_buf_noundo(cmd->buf, TRUE);
	cmd->exec = BUF_DELETE_BUF_ACTION;
    }
    move_cmd(cmd);
}

static void buf_delete_buf_action(undo_cmd_t *cmd)
{
    buf_delete_buf_noundo(cmd->buf, TRUE);
    cmd->exec = BUF_RESTORE_BUF_ACTION;
    move_cmd(cmd);
}

static void buf_remove_buf_action(undo_cmd_t *cmd)
{
    buf_delete_buf_noundo(cmd->buf, FALSE);
    cmd->exec = BUF_RESTORE_BUF_ACTION;
    move_cmd(cmd);
}

const action BUF_APPENDS_ACTION = buf_appends_action;
const action BUF_APPENDC_ACTION = buf_appendc_action;
const action BUF_INSERTS_ACTION = buf_inserts_action;
const action BUF_INSERTC_ACTION = buf_insertc_action;
const action BUF_DELETE_ACTION = buf_delete_action;
const action BUF_REPLACE_ACTION = buf_replace_action;
const action BUF_RESTORE_BUF_ACTION = buf_restore_buf_action;
const action BUF_DELETE_BUF_ACTION = buf_delete_buf_action;
const action BUF_REMOVE_BUF_ACTION = buf_remove_buf_action;

void undo_cmd_add(action act, buf_t *buf, char *data, int start, 
		  int end, int left)
{
    undo_cmd_t *cmd = NULL;
    
    if ((cmd = (undo_cmd_t *)malloc(sizeof(undo_cmd_t))) == NULL) {
	perror("Out of memory!");
	exit(0);
    }

    cmd->exec = act;
    cmd->buf = buf;
    cmd->data = data;
    cmd->start = start;
    cmd->end = end;
    cmd->left = left;
    INIT_LIST_HEAD(&(cmd->list));

    if (current_window->undo_cmd != NULL) {
	list_add(&(cmd->list), &(current_window->undo_cmd->list));
    }
    current_window->undo_cmd = cmd;
}

static int exec_cmd(undo_cmd_t **pcmd) {
    undo_cmd_t *cmd = NULL;

    if (*pcmd == NULL) {
	return UNDO_FAIL;
    }

    cmd = list_entry((*pcmd)->list.prev, undo_cmd_t, list);
    (*pcmd)->exec(*pcmd);

    if (*pcmd == cmd) {
	*pcmd = NULL;
    } else {
	*pcmd = cmd;
    }

    return UNDO_OK;
}

static int exec_cmds(undo_cmd_t **pcmd) {
    int n = 0;
    int i = 0;

    if (*pcmd == NULL) {
	return UNDO_FAIL;
    }

    n = (*pcmd)->left;

    for (i = 0; i < n; i++) {
	(*pcmd)->left = i + 1;
	if (exec_cmd(pcmd) == UNDO_FAIL)  
	    return UNDO_FAIL;
    }
    return UNDO_OK;
}

int undo(void) {
    return exec_cmds(&current_window->undo_cmd);
}

int redo(void) {
    return exec_cmds(&current_window->redo_cmd);
}

void undo_cmd_clear_cmds(undo_cmd_t *cmd_head)
{
    struct list_head *pos;
    struct list_head *n;
    
    if (cmd_head == NULL)
	return;
    list_for_each_safe(pos, n, &(cmd_head->list)) {
	undo_cmd_t *cmd = list_entry(pos, undo_cmd_t, list);
	free(cmd->data);
	free(cmd);
	list_del(pos);
    }
}
